package org.melanesia.sql.utils;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * This is a helper class allowing to use named parameters binding for queries,
 * calls and batches. This class converts a query with named parameters into a
 * query with indexed parameters and stores namer parameters indices.
 *
 * Then, it allows to set parameter in a named fashion, implicitly setting them
 * in indexed fashion.
 *
 * @author marcin.kielar
 */
public class NamedParametersHelper {

    /** Maps parameter names to arrays of ints which are the parameter indices. */
    private final Map<String, List<Integer>> paramMap = new LinkedHashMap<String, List<Integer>>();

    /** Parsed query. */
    private final String parsedSql;

    /** Original query. */
    private final String originalSql;

    /**
     * Constructs new NamedParametersHelper.
     *
     * @param sql
     *            sql statement
     */
    public NamedParametersHelper(final String sql) {
        this.originalSql = sql;
        this.parsedSql = parse(sql);
    }

    /**
     * Parses a query with named parameters. The parameter-index mappings are
     * put into the map, and the parsed query is returned. DO NOT CALL FROM
     * CLIENT CODE. This method is non-private so JUnit code can test it.
     *
     * @param query
     *            query to parse
     * @return the parsed query
     */
    private String parse(final String query) {

        // I was originally using regular expressions, but they didn't work well
        // for ignoring parameter-like strings inside quotes.

        int length = query.length();
        StringBuffer parsedQuery = new StringBuffer(length);
        boolean inSingleQuote = false;
        boolean inDoubleQuote = false;
        boolean inSingleLineComment = false;
        boolean inMultiLineComment = false;

        int index = 1;

        for (int i = 0; i < length; i++) {
            char c = query.charAt(i);
            char c1 = (i < length - 1) ? query.charAt(i + 1) : (char) 0;

            if (inSingleQuote) {
                if (c == '\'') {
                    inSingleQuote = false;
                }
            } else if (inDoubleQuote) {
                if (c == '"') {
                    inDoubleQuote = false;
                }
            } else if (inSingleLineComment) {
                if (c == '\r' || c == '\n') {
                    inSingleLineComment = false;
                }
            } else if (inMultiLineComment) {
                if (c == '*' && c1 == '/') {
                    inMultiLineComment = false;
                }
            } else {
                if (c == '\'') {
                    inSingleQuote = true;
                } else if (c == '"') {
                    inDoubleQuote = true;
                } else if (c == '-' && c1 == '-') {
                    inSingleLineComment = true;
                } else if (c == '/' && c1 == '*') {
                    inMultiLineComment = true;
                } else if (c == ':' && i + 1 < length
                        && Character.isJavaIdentifierStart(query.charAt(i + 1))) {
                    int j = i + 2;
                    while (j < length
                            && Character.isJavaIdentifierPart(query.charAt(j))) {
                        j++;
                    }
                    String name = query.substring(i + 1, j);
                    c = '?'; // replace the parameter with a question mark
                    i += name.length(); // skip past the end if the parameter

                    List<Integer> indexList = paramMap.get(name);
                    if (indexList == null) {
                        indexList = new LinkedList<Integer>();
                        paramMap.put(name, indexList);
                    }
                    indexList.add(new Integer(index));

                    index++;
                }
            }
            parsedQuery.append(c);
        }

        // replace the lists of Integer objects with arrays of ints
        for (Map.Entry<String, List<Integer>> entry : paramMap.entrySet()) {

            List<Integer> list = entry.getValue();
            List<Integer> indexes = new LinkedList<Integer>();

            for (Integer i : list) {
                indexes.add(i);
            }

            entry.setValue(indexes);
        }

        return parsedQuery.toString();
    }

    /**
     * Returns true if given parameter exists in the sql.
     *
     * @param parameterName
     *            parameter name
     * @return true if parameter with such name exists, false otherwise
     */
    public final boolean isParameter(final String parameterName) {
        return paramMap.containsKey(parameterName);
    }

    /**
     * Returns the indexes for a parameter.
     *
     * @param parameterName
     *            parameter name
     * @return parameter indices
     */
    public final List<Integer> getIndices(final String parameterName) {
        List<Integer> indexes = paramMap.get(parameterName);
        if (indexes == null) {
            throw new IllegalArgumentException("Parameter not found: " + parameterName);
        }
        return indexes;
    }

    /**
     * Returns parsed SQL with indexed parameters.
     * @return parsed SQL
     */
    public final String getSql() {
        return parsedSql;
    }

    /**
     * Returns original SQL.
     * @return original SQL
     */
    public final String getOriginalSql() {
        return originalSql;
    }

    /**
     * Generates unique parameter names that will not collide with currently
     * existing parameters in the query. Useful for automatic query wrapping and
     * adding additional parameters (like for example implementing pagination).
     *
     * @param prefix
     *            obligatory parameter prefix
     * @param count
     *            number of parameters
     * @return list of generated parameter names
     */
    public final String[] generateUniqueNamedParameterNames(final String prefix, final int count) {

        List<String> names = new ArrayList<String>();
        Set<String> used = new HashSet<String>(paramMap.keySet());

        for (int i = 0; i < count; i++) {

            int next = 0;

            while (true) {
                String nameProposal = prefix + next;
                if (used.contains(nameProposal)) {
                    next++;
                    continue;
                } else {
                    names.add(nameProposal);
                    used.add(nameProposal);
                    break;
                }
            }
        }

        return names.toArray(new String[0]);
    }
}
